package junior.db.dao.jdbc;

import junior.db.annotation.EnumValue;
import junior.db.annotation.IEnumValue;
import junior.db.annotation.PersistentValue;
import junior.util.exception.ExceptionUtils;
import junior.util.function.CheckedFunction;
import junior.util.reflect.ReflectUtils;
import junior.util.value.ValueUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author wubo
 * @date 2024/9/4 17:52
 */
public class JdbcHelper {
    private static final ConcurrentHashMap<Class<?>, Pair<Class<?>, CheckedFunction<Object, Object>>> PERSIST_FROM_INFO_CACHE = new ConcurrentHashMap<>();
    private static final ConcurrentHashMap<Class<?>, Pair<Class<?>, CheckedFunction<Object, Object>>> PERSIST_TO_INFO_CACHE = new ConcurrentHashMap<>();
    
    public static void setValue(PreparedStatement ps, Object value, int index) throws SQLException {
        if (value == null) {
            ps.setNull(index, java.sql.Types.NULL);
            return;
        }
        
        Class type = value.getClass();
        
        Pair<Class<?>, CheckedFunction<Object, Object>> info = getPersistToInfo(type);
        if (info != null) {
            type = info.getLeft();
            value = info.getRight().apply(value);
        }
        
        if (int.class.isAssignableFrom(type)) {
            ps.setInt(index, (Integer) value);
        } else if (String.class.isAssignableFrom(type)) {
            ps.setString(index, ValueUtils.convert(value, String.class));
        } else if (long.class.isAssignableFrom(type)) {
            ps.setLong(index, (Long) value);
        } else if (boolean.class.isAssignableFrom(type)) {
            ps.setBoolean(index, (Boolean) value);
        } else if (double.class.isAssignableFrom(type)) {
            ps.setDouble(index, (Double) value);
        } else if (char.class.isAssignableFrom(type)) {
            ps.setString(index, (Character) value + "");
        } else if (short.class.isAssignableFrom(type)) {
            ps.setShort(index, (Short) value);
        } else if (float.class.isAssignableFrom(type)) {
            ps.setFloat(index, (Float) value);
        } else if (byte.class.isAssignableFrom(type)) {
            ps.setByte(index, (Byte) value);
        } else if (value instanceof java.sql.Timestamp) {
            ps.setTimestamp(index, (java.sql.Timestamp) value);
        } else if (value instanceof java.sql.Date) {
            ps.setDate(index, (java.sql.Date) value);
        } else if (value instanceof java.sql.Time) {
            ps.setTime(index, (java.sql.Time) value);
        } else if (value instanceof java.math.BigDecimal) {
            ps.setBigDecimal(index, (java.math.BigDecimal) value);
        } else if (value instanceof java.math.BigInteger) {
            ps.setLong(index, ((java.math.BigInteger) value).longValue());
        } else {
            ps.setObject(index, value);
        }
    }
    
    public static Object getValue(ResultSet rs, int index, Class type) throws SQLException {
        boolean wrapperType = org.springframework.util.ClassUtils.isPrimitiveWrapper(type);
        if (wrapperType) {
            org.springframework.util.ClassUtils.resolvePrimitiveIfNecessary(type);
        }
        Class valueType = null;
        CheckedFunction<Object, Object> from = null;
        
        Pair<Class<?>, CheckedFunction<Object, Object>> info = getPersistFromInfo(type);
        if (info != null) {
            valueType = info.getLeft();
            from = info.getRight();
        }
        
        Object value = null;
        if (int.class.isAssignableFrom(valueType)) {
            value = rs.getInt(index);
        } else if (String.class.isAssignableFrom(valueType)) {
            value = rs.getString(index);
        } else if (long.class.isAssignableFrom(valueType)) {
            value = rs.getLong(index);
        } else if (boolean.class.isAssignableFrom(valueType)) {
            value = rs.getBoolean(index);
        } else if (double.class.isAssignableFrom(valueType)) {
            value = rs.getDouble(index);
        } else if (char.class.isAssignableFrom(valueType)) {
            value = rs.getString(index);
        } else if (short.class.isAssignableFrom(valueType)) {
            value = rs.getShort(index);
        } else if (float.class.isAssignableFrom(valueType)) {
            value = rs.getFloat(index);
        } else if (byte.class.isAssignableFrom(valueType)) {
            value = rs.getByte(index);
        } else if (java.sql.Timestamp.class.isAssignableFrom(valueType)) {
            value = rs.getTimestamp(index);
        } else if (java.sql.Date.class.isAssignableFrom(valueType)) {
            value = rs.getDate(index);
        } else if (java.util.Date.class.isAssignableFrom(valueType)) {
            value = rs.getTimestamp(index);
        } else if (java.sql.Time.class.isAssignableFrom(valueType)) {
            value = rs.getTime(index);
        } else {
            value = rs.getObject(index);
        }
        if (rs.wasNull() && wrapperType) {
            return null;
        }
        if (from != null) {
            value = from.apply(value);
        } else if (value != null && type.isEnum()) {
            value = Enum.valueOf(type, value.toString());
        }
        
        return value;
    }
    
    public static Object getValue(ResultSet rs, String name, Class type) throws SQLException {
        boolean wrapperType = org.springframework.util.ClassUtils.isPrimitiveWrapper(type);
        if (wrapperType) {
            org.springframework.util.ClassUtils.resolvePrimitiveIfNecessary(type);
        }
        Class valueType = type;
        CheckedFunction<Object, Object> from = null;
        
        Pair<Class<?>, CheckedFunction<Object, Object>> info = getPersistFromInfo(type);
        if (info != null) {
            valueType = info.getLeft();
            from = info.getRight();
        }
        
        Object value = null;
        if (int.class.isAssignableFrom(valueType)) {
            value = rs.getInt(name);
        } else if (String.class.isAssignableFrom(valueType)) {
            value = rs.getString(name);
        } else if (long.class.isAssignableFrom(valueType)) {
            value = rs.getLong(name);
        } else if (boolean.class.isAssignableFrom(valueType)) {
            value = rs.getBoolean(name);
        } else if (double.class.isAssignableFrom(valueType)) {
            value = rs.getDouble(name);
        } else if (char.class.isAssignableFrom(valueType)) {
            value = rs.getString(name);
        } else if (short.class.isAssignableFrom(valueType)) {
            value = rs.getShort(name);
        } else if (float.class.isAssignableFrom(valueType)) {
            value = rs.getFloat(name);
        } else if (byte.class.isAssignableFrom(valueType)) {
            value = rs.getByte(name);
        } else if (java.sql.Timestamp.class.isAssignableFrom(valueType)) {
            value = rs.getTimestamp(name);
        } else if (java.sql.Date.class.isAssignableFrom(valueType)) {
            value = rs.getDate(name);
        } else if (java.util.Date.class.isAssignableFrom(valueType)) {
            value = rs.getTimestamp(name);
        } else if (java.sql.Time.class.isAssignableFrom(valueType)) {
            value = rs.getTime(name);
        } else {
            value = rs.getObject(name);
        }
        if (rs.wasNull() && wrapperType) {
            return null;
        }
        if (from != null) {
            value = from.apply(value);
        } else if (value != null && type.isEnum()) {
            value = Enum.valueOf(type, value.toString());
        }
        
        return value;
    }
    
    public static Pair<Class<?>, CheckedFunction<Object, Object>> getPersistFromInfo(final Class<?> type) {
        return PERSIST_FROM_INFO_CACHE.computeIfAbsent(type, (key) -> {
            if (type.isAnnotationPresent(PersistentValue.class)) {
                PersistentValue pv = type.getAnnotation(PersistentValue.class);
                return Pair.of(pv.type(), (val) -> fromPersistValue(pv, type, val));
            } else if (type.isEnum()) {
                if (IEnumValue.class.isAssignableFrom(type)) {
                    Class<?> valueType = ReflectUtils.getInterfaceGenericType(type, IEnumValue.class, 0);
                    return Pair.of(valueType, (val) -> valueOfEnumValue((Class<? extends Enum>) type, val));
                } else {
                    List<Field> fieldList = FieldUtils.getFieldsListWithAnnotation(type, EnumValue.class);
                    if (fieldList.size() > 0) {
                        Field f = fieldList.get(0);
                        return Pair.of(f.getType(), (val) -> valueOfEnumValue((Class<? extends Enum>) type, f, val));
                    }
                }
                return Pair.of(type, (val) -> Enum.valueOf((Class<? extends Enum>) type, (String) val));
            }
            return null;
        });
    }
    
    public static Pair<Class<?>, CheckedFunction<Object, Object>> getPersistToInfo(final Class<?> type) {
        return PERSIST_TO_INFO_CACHE.computeIfAbsent(type, (key) -> {
            if (type.isAnnotationPresent(PersistentValue.class)) {
                PersistentValue pv = type.getAnnotation(PersistentValue.class);
                return Pair.of(pv.type(), (val) -> toPersistValue(pv, type, val));
            } else if (type.isEnum()) {
                if (IEnumValue.class.isAssignableFrom(type)) {
                    Class<?> valueType = ReflectUtils.getInterfaceGenericType(type, IEnumValue.class, 0);
                    return Pair.of(valueType, (val) -> ((IEnumValue) val).getValue());
                } else {
                    List<Field> fieldList = FieldUtils.getFieldsListWithAnnotation(type, EnumValue.class);
                    if (fieldList.size() > 0) {
                        Field f = fieldList.get(0);
                        return Pair.of(f.getType(), (val) -> FieldUtils.readField(f, val, true));
                    }
                }
                return Pair.of(type, (val) -> ((Enum) val).name());
            }
            return null;
        });
    }
    
    public static Object toPersistValue(PersistentValue pv, Class<?> type, Object obj) {
        try {
            Method m = ReflectUtils.getDeclaredMethod(type, pv.toMethod(), true);
            return m.invoke(obj);
        } catch (Exception e) {
            throw ExceptionUtils.wrap2Runtime(e);
        }
    }
    
    public static Object fromPersistValue(PersistentValue pv, Class<?> type, Object value) {
        try {
            Method m = ReflectUtils.getDeclaredMethod(type, pv.fromMethod(), true, pv.type());
            return m.invoke(type, ValueUtils.convert(value, pv.type()));
        } catch (Exception e) {
            throw ExceptionUtils.wrap2Runtime(e);
        }
    }
    
    private static <E extends Enum> Object valueOfEnumValue(Class<E> type, Object value) {
        E[] array = type.getEnumConstants();
        return Arrays.stream(array).filter(e -> equalsValue(((IEnumValue) e).getValue(), value)).findAny().orElse(null);
    }
    
    private static <E extends Enum> Object valueOfEnumValue(Class<E> type, Field valueField, Object value) throws IllegalAccessException {
        E[] array = type.getEnumConstants();
        for (E e : array) {
            if (equalsValue(FieldUtils.readField(valueField, e, true), value)) {
                return e;
            }
        }
        return null;
    }
    
    private static boolean equalsValue(Object v1, Object v2) {
        if (v1 == null || v2 == null) {
            return true;
        }
        if ((v1 == null && v2 != null) || (v1 != null && v2 == null)) {
            return false;
        }
        return Objects.equals(v1.toString().trim(), v2.toString().trim());
    }
}
